4G USB共享网卡¶
Copyright © Quectel Wireless Solutions Co., Ltd. 2026. All rights reserved.
本方案基于UniRTOS构建,实现4G共享上网与本地Portal管理。
USBNET Demo基于UniRTOS运行,结合网络附着、PDP配置、Datacall建立、USBNET共享、子网配置与NAT转发机制,在移远通信模块上完成USB虚拟网卡共享上网、本地页面管理与运行状态展示。
系统支持在模块侧建立蜂窝数据链路,并在数据链路建立完成后启动USBNET共享,使Windows、Linux、Android终端能将设备识别为虚拟网卡并具备上网能力;同时支持在本地网段开放Portal服务,通过浏览器访问管理页面,查看设备信息、网络状态、上下行速率以及今日/本周/本月流量统计,并可执行连接、断开、重启设备、切换协议等控制操作。
开发资源¶
资源名称 |
数量 |
简要描述 |
获取方法 |
|---|---|---|---|
pico开发板 |
1 |
项目运行的硬件平台 |
|
USB数据线 |
1 |
用于连接PC和开发板,非“仅充电”类型USB-A转USB-C。 |
|
PC |
1 |
系统为Windows 10/Windows 11。 |
自行准备 |
SIM卡 |
1 |
可正常上网的USIM卡。 |
自行准备 |
快速上手¶
硬件连接¶
将SIM卡插入到开发板SIM卡槽内。
使用数据线连接开发板和电脑。
软件部署¶
开发环境搭建¶
参见 快速上手,搭建开发环境并完成基本开发流程。
代码拉取¶
打开一个新的PowerShell窗口,执行以下命令:
# 拉取示例仓库
unirtos-cli new -r unirtos-maker-examples
# 进入该项目
cd unirtos-maker-examples/4g_donlg
修改配置参数¶
在打开的PowerShell窗口继续执行环境配置命令:
unirtos-cli env-setup
在PowerShell窗口执行固件编译命令(若模块型号非EG800ZCN_LA,请替换为实际型号):
unirtos-cli build -m EG800ZCN_LA -v EG800ZCN_LA_BETA_20260904
等待编译结束后,编译输出末尾会提示固件编译结果:
SUCCESS: Unirtos project built successfully!
编译成功后,使用QFlash将固件烧录到开发板。
板上验证¶
长按开发板电源键(PWKY)开机,打开EPAT工具,根据 日志调试 进行配置,配置完成后重启开发板,从模块上电开始抓取模块日志,确保日志输出完整。
在网络适配器中确认是否出现Remote NDIS based Internet Sharing Device设备。
打开浏览器访问192.168.6.1,能打开内置页面即表示联网成功。
软件设计详解¶
方案框架¶
业务流程¶
启动流程¶
启动流程说明
系统启动后,应用入口函数 unir_usbnet_demo_init() 通过 UNIRTOS_APP_EXPORT() 自动注册并执行。
unir_usbnet_demo_init() 负责检查主任务是否已存在;若不存在,则创建 qosa_usbnet_demo_task() 主任务。
qosa_usbnet_demo_task() 启动后,先初始化运行参数,读取当前USBNET类型,并在类型不符合预期时执行 qosa_usbnet_set_type() 和设备复位。
当USBNET类型满足要求后,主任务继续等待网络附着,配置PDP Context,设置USB子网,启用NAT,建立Datacall,并启动USBNET共享链路。
USBNET启动成功后,程序初始化Portal状态缓存,并依次创建状态采集任务 qosa_usbnet_status_task() 和Portal服务任务 qosa_usbnet_portal_task();若已启用应用层DNS方案,仍会继续创建DNS任务。
主任务完成初始化后进入监督循环,持续处理待执行动作、执行策略检查,并维持服务运行;由此,Portal任务既能响应页面控制请求,也不必直接承担底层链路的控制压力。
代码讲解¶
项目结构¶
usbnet/
├── CMakeLists.txt # 工程构建入口
├── env_config.json # 构建环境配置
├── usb_net.c # USBNET Demo核心业务实现
├── menuconfig/ # 配置目录
├── qos_build/ # 构建输出目录
├── release/ # 打包产物目录
└── tools/ # 辅助工具目录
入口初始化¶
unir_usbnet_demo_init():
打印启动日志。
检查USBNET Demo主任务是否已存在。
调用 qosa_task_create() 创建 qosa_usbnet_demo_task()。
void unir_usbnet_demo_init(void)
{
int err = 0;
### 主任务与建链逻辑
if (g_usbnet_demo_task_ptr == QOSA_NULL)
{
QLOGI("create usbnet demo task");
err = qosa_task_create(&g_usbnet_demo_task_ptr, 4 * 1024, QOSA_PRIORITY_NORMAL, "usbnet_demo", qosa_usbnet_demo_task, QOSA_NULL);
if (err != QOSA_OK)
{
QLOGI("USBNET demo init task create error");
return;
}
QLOGI("create usbnet demo task ok");
}
return;
}
UNIRTOS_APP_EXPORT(200, "usb_net", unir_usbnet_demo_init);
上述代码通过UniRTOS的应用注册宏 UNIRTOS_APP_EXPORT(),将USBNET Demo的入口函数注册到系统应用初始化列表中,由框架统一调度。
主控制与建链逻辑¶
USBNET Demo的主控制逻辑位于 usb_net.c 的 qosa_usbnet_demo_task(),链路建立的入口位于 usb_net.c 的 _usbnet_demo_connect_data()。
static qosa_bool_t _usbnet_demo_connect_data(void)
{
qosa_int32_t ret = 0;
qosa_datacall_conn_t conn = {0};
qosa_usbnet_config_t *config = QOSA_NULL;
conn = qosa_datacall_conn_new(g_usbnet_demo_portal_simid, g_usbnet_demo_portal_profile_idx, QOSA_DATACALL_CONN_UNDEFINED);
if (!qosa_datacall_get_status(conn))
{
_usbnet_demo_configure_pdp_context(g_usbnet_demo_portal_simid, g_usbnet_demo_portal_profile_idx);
ret = qosa_datacall_start(conn, USBNET_DEMO_WAIT_ATTACH_MAX_WAIT_TIME);
if (ret != QOSA_DATACALL_OK)
{
QLOGI("portal connect datacall fail, ret=%d", ret);
return QOSA_FALSE;
}
}
if (!_usbnet_demo_apply_usbnet_config(g_usbnet_demo_method))
{
QLOGI("portal apply usbnet config fail");
return QOSA_FALSE;
}
_usbnet_demo_configure_usbnet_subnet(g_usbnet_demo_portal_simid);
_usbnet_demo_log_usbnet_subnet(g_usbnet_demo_portal_simid);
_usbnet_demo_enable_nat(g_usbnet_demo_portal_simid, g_usbnet_demo_portal_profile_idx);
_usbnet_demo_cache_upstream_dns(g_usbnet_demo_portal_simid, g_usbnet_demo_portal_profile_idx);
config = (qosa_usbnet_config_t *)qosa_malloc(sizeof(qosa_usbnet_config_t));
if (config == QOSA_NULL)
{
return QOSA_FALSE;
}
config->simid = g_usbnet_demo_portal_simid;
config->pdpid = g_usbnet_demo_portal_profile_idx;
config->method = g_usbnet_demo_method;
ret = qosa_usbnet_start(g_usbnet_demo_portal_simid, _usbnet_demo_func_cb, config);
if (ret != QOSA_USBNET_ERR_OK)
{
QLOGI("portal usbnet start fail, ret=%d", ret);
qosa_free(config);
return QOSA_FALSE;
}
return QOSA_TRUE;
}
上述代码体现了USBNET Demo的主链路策略:先建立蜂窝数据出口,再配置USB本地子网并启用NAT,最后启动USBNET共享。Portal页面与状态服务依赖底层链路的可用性。
状态模型与缓存¶
页面展示依赖统一状态对象 usbnet_demo_portal_status_t。该结构定义位于 usb_net.c,主要聚合以下几类信息:
设备信息:型号、固件版本。
网络信息:网络状态、运营商、网络制式、USBNET状态。
流量信息:上下行速率、今日/本周/本月统计。
控制信息:自动重连、策略状态、活动设备、限制额度。
原始统计字段:总流量、上下行字节、时间有效标记。
Portal页面所需的动态数据,统一由 usb_net.c 的 _usbnet_demo_collect_portal_status() 和 usb_net.c 的 _usbnet_demo_collect_portal_status_light() 采集,再通过缓存接口集中保存。
static void _usbnet_demo_collect_portal_status(usbnet_demo_portal_status_t *status)
{
qosa_int32_t len = 0;
qosa_uint8_t ps_status = QOSA_NW_REG_UNKNOWN;
qosa_datacall_conn_t conn = {0};
qosa_datacall_traffic_statistics_t traffic_statistics = {0};
qosa_nw_oper_name_t short_name = {0};
qosa_usbnet_status_e usbnet_status = QOSA_USBNET_STATUS_IDLE;
if (status == QOSA_NULL)
{
return;
}
_usbnet_demo_init_portal_status_defaults(status);
/* 后续依次采集型号、版本、运营商、注册状态、网络制式、USBNET状态、Datacall状态和流量统计 */
}
usb_net.c 中的状态采集任务 qosa_usbnet_status_task() 采用“重采集+轻采集”双路径模型:低频周期执行全量状态更新,高频周期只刷新时间、流量这类热点字段。由此,既能保持页面近实时刷新,又能降低对底层接口的查询压力。
对应的缓存读写函数位于 usb_net.c。这种“先采集、再缓存、后输出”的组织方式,使页面渲染和状态采样解耦,避免多个HTTP请求同时直接访问底层接口。
Portal页面与状态接口¶
Portal服务主入口位于 usb_net.c 的 qosa_usbnet_portal_task(),请求处理主逻辑位于 usb_net.c 的 _usbnet_demo_handle_portal_client()。
static void qosa_usbnet_portal_task(void *arg)
{
qosa_int32_t portal_fd = -1;
qosa_int32_t captive_fd = -1;
qosa_int32_t client_fd = -1;
QOSA_UNUSED(arg);
QLOGI("usbnet portal task start");
while (g_usbnet_demo_service_running)
{
portal_fd = _usbnet_demo_open_listen_socket(USBNET_DEMO_PORTAL_PORT);
captive_fd = _usbnet_demo_open_listen_socket(USBNET_DEMO_CAPTIVE_PORT);
/* 通过select/accept接收客户端请求,并调用_usbnet_demo_handle_portal_client()处理 */
}
}
Portal服务承担两类输出:
页面输出:返回Portal管理页HTML。
数据输出:返回 /status JSON。
其中 /status JSON的生成由 usb_net.c 的 _usbnet_demo_build_status_json() 完成,页面中的网络状态、上下行速率、今日/本周/本月流量、策略状态和通知信息均来自该函数。
页面并非独立前端工程,而是由C代码在运行时拼接HTML、CSS和JavaScript后直接返回浏览器;这种方式部署简单、资源集中,但模板较长、维护成本偏高。为此,代码按功能将模板拆分为多段拼接,以降低超长格式串带来的风险。
控制动作与监督循环¶
Portal页面支持连接、断开、重启、协议切换等控制动作,但这些动作不会直接在HTTP请求处理中执行,而是先入队,再由主任务统一处理。
static void _usbnet_demo_process_pending_action(void)
{
usbnet_demo_portal_action_e action = g_usbnet_demo_pending_action;
if (action == USBNET_DEMO_PORTAL_ACTION_NONE)
{
return;
}
g_usbnet_demo_pending_action = USBNET_DEMO_PORTAL_ACTION_NONE;
g_usbnet_demo_action_busy = QOSA_TRUE;
switch (action)
{
case USBNET_DEMO_PORTAL_ACTION_CONNECT:
_usbnet_demo_set_notice(_usbnet_demo_connect_data() ? "Connect request executed." : "Connect request failed.");
break;
case USBNET_DEMO_PORTAL_ACTION_DISCONNECT:
_usbnet_demo_set_notice("Disconnect request executing.");
_usbnet_demo_disconnect_data();
break;
case USBNET_DEMO_PORTAL_ACTION_REBOOT:
_usbnet_demo_set_notice("Reboot request executing.");
qosa_power_reset(QOSA_RESET_NORMAL);
break;
default:
break;
}
g_usbnet_demo_action_busy = QOSA_FALSE;
}
这种设计体现了典型的嵌入式控制原则:Portal任务只负责接收请求,耗时动作由主任务统一串行执行,从而降低HTTP请求与底层链路控制并发执行的风险。
网络限制与策略设计¶
网络限制能力由 usbnet_demo_access_policy_t 和 _usbnet_demo_apply_policy_request() 组成,主要支持以下配置:
是否允许网络访问。
是否启用时间计划。
起止时间窗口。
总流量限制。
Windows / Linux设备单独限制。
static void _usbnet_demo_apply_policy_request(const char *request, char *message, qosa_uint32_t message_len)
{
usbnet_demo_access_policy_t next_policy = g_usbnet_demo_policy;
char value[24] = {0};
if (_usbnet_demo_extract_query_value(request, "network_enabled", value, sizeof(value)))
{
next_policy.network_enabled = _usbnet_demo_parse_bool_value(value, next_policy.network_enabled);
}
/* 后续继续解析schedule、limit、device policy等字段 */
g_usbnet_demo_policy = next_policy;
_usbnet_demo_set_text(message, message_len, "Network policy saved.");
}
当前实现中,策略保存、页面展示和状态回显均已实现,但 usb_net.c 的 _usbnet_demo_enforce_policy() 仍采用保守实现,没有真正执行硬阻断。这意味着当前版本定位为“管理配置+状态展示”的Demo,而不是完整的接入控制系统。
异步回调与资源回收¶
USBNET异步回调位于 usb_net.c 的 _usbnet_demo_func_cb()。
static void _usbnet_demo_func_cb(void *ctx, void *argv)
{
qosa_usbnet_config_t *usbnet_config = (qosa_usbnet_config_t *)ctx;
qosa_usbnet_general_cnf_t *cnf = argv;
QLOGI("usbnet callback enter");
if ((cnf != QOSA_NULL) && (QOSA_OK == cnf->err_code))
{
QLOGI("usbnet callback success");
}
else
{
QLOGI("usbnet callback failed");
}
qosa_free(usbnet_config);
}
该回调不负责业务决策,主要职责是接收SDK异步结果、打印日志并回收 qosa_usbnet_config_t。从职责划分上看,可视为USBNET控制链路的完成通知与资源释放点。
应用演示¶
为了更直观地体现USBNET Demo的跨平台适配能力,演示场景分别针对Windows PC、Linux(Ubuntu)和Android三类终端展开。三类终端均通过USB接入模块侧USBNET共享网络,在终端侧获取本地网段地址后访问Portal页面,展示“可识别、可上网、可管理、可控制”的完整闭环。
Windows PC平台演示效果¶
PC识别USB虚拟网卡,在设备管理器-网络适配器中能够看到NDIS,即表示识别成功。
打开 cmd 并执行 ipconfig,确认已获取基本网络信息。此时关闭其余网络接口,仅保留USB网卡的网络接口,电脑仍可正常上网。
浏览器访问Portal首页,可实时查看设备型号、网络状态、USBNET状态、运营商、运行时间及流量统计等信息,即表示可正常访问网络。
Linux(Ubuntu)平台演示效果¶
在Linux(Ubuntu)环境下接入模块后,需要按下按键切换ECM协议模式。
将设备的USB接口接入Linux系统。
网卡加载后,通过 ifconfig 命令查看网卡状态,确认新增usb1网卡。如下图所示:
确认网络连接。
通过 ping www.baidu.com确认网络连通性。如下图所示:
浏览器访问Portal首页,可实时查看设备型号、网络状态、USBNET状态、运营商、运行时间及流量统计等信息。
Android平台演示效果¶
在Android终端环境下,可通过支持OTG的手机连接USB网卡。
Android终端接入设备后,需要打开OTG模式。
模块连接手机并上电后,手机状态栏会显示<···>图标,表示已识别到USB网卡,此时可通过浏览器正常访问网络。
浏览器访问Portal首页,可实时查看设备型号、网络状态、USBNET状态、运营商、运行时间及流量统计等信息。